/* This file is part of FireEscape.
 *
 * FireEscape is the intellectual work of Phillip Cheng & Allen Park.
 * All future releases or modifications of this program must include this
 * attribuation as according to the Creative Commons BY-SA license.
 *
 * FireEscape is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * FireEscape is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with FireEscape.  If not, see <http://www.gnu.org/licenses/>.
 *
 * All modifications to this program should include this original statement.
 *
 * If you modify, distribute, or use the program , please contact
 * fireescape@kloudvine.com
 * http://code.google.com/p/fireescape/
 *
 * If you are interested in using FireEscape for your structure, please contact us
 * for support.
 *
 * If you do use FireEscape, please consider donating to the project.
 */
package fireescape;

import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class creates a Bot
 * @author chengP
 */
public class Bot {

    private int routeID;
    public final int botID;
    private double xCoord;
    private double yCoord;
    private double direction;
    private double idealDirection;
    private double velocity;
    private int roomID;
    private int lastRoomID;
    private int currentRouteWaypoint = 0;
    private int staircaseID;
    private int howLongSameRoom;
    private double destX;
    private double destY;
    private double random;
    private boolean exiting;
    private boolean onStairs;
    private boolean transferringToStaircase;
    private boolean escaped;
    //private boolean approachingExit;
    private ArrayList<Integer> immediateNeighbors;
    //private Bot nearestNeighbor;

    /**
     * Constructs the bot
     * @param botID The bot ID
     * @param routeID The route ID
     * @param roomID The starting room ID
     * @param xCoord The start x coordinate
     * @param yCoord The start y coordinate
     */
    public Bot(int botID, int routeID, int roomID, double xCoord, double yCoord) {
        this.botID = botID;
        this.routeID = routeID;
        this.xCoord = xCoord;
        this.yCoord = yCoord;
        this.roomID = roomID;
        lastRoomID = -1;
        staircaseID = -1;
        howLongSameRoom = 0;
        exiting = false;
        onStairs = false;
        transferringToStaircase = false;
        //approachingExit = false;
        random = Math.random();
        immediateNeighbors = new ArrayList<Integer>();
        //nearestNeighbor = null;
        setDestinations();
        setIdealDirection();
    }

    /**
     * Returns bot ID
     * @return The bot ID
     */
    public int getID() {
        return this.botID;
    }

    /**
     * Sets the destination of the bot depending on the next exit in the route
     */
    public void setDestinations() {
        Exit botRouteWaypoint = getCurrentWaypoint();

        double x1 = botRouteWaypoint.getX1();
        double y1 = botRouteWaypoint.getY1();
        double x2 = botRouteWaypoint.getX2();
        double y2 = botRouteWaypoint.getY2();
        double dista = FireEscapeGUI.distance(x1, y1, xCoord, yCoord);
        double distb = FireEscapeGUI.distance(x2, y2, xCoord, yCoord);
        double distc = FireEscapeGUI.distance(botRouteWaypoint.getCenterX(), botRouteWaypoint.getCenterY(), xCoord, yCoord);
        if ((xCoord < Math.max(x1, x2) && xCoord > Math.min(x1, x2) && y2 == y1)) {
            destX = xCoord;
            destY = y1;
        } else if (yCoord < Math.max(y1, y2) && yCoord > Math.min(y1, y2) && x2 == x1) {
            destX = x1;
            destY = yCoord;
        } else if (dista > distc) {
            destX = botRouteWaypoint.getX2() + (1.0 / 3 * random) * (x1 - x2);
            destY = botRouteWaypoint.getY2() + (1.0 / 3 * random) * (y1 - y2);
        } else if (distb > distc) {
            destX = botRouteWaypoint.getX1() + (1.0 / 3 * random) * (x2 - x1);
            destY = botRouteWaypoint.getY1() + (1.0 / 3 * random) * (y2 - y1);
        } else {
            destX = botRouteWaypoint.getX1() + (1.0 / 3 * random + 1.0 / 3) * (x2 - x1);
            destY = botRouteWaypoint.getY1() + (1.0 / 3 * random + 1.0 / 3) * (y2 - y1);
        }
    }

    /**
     * Returns the x coordinate of the destination
     * @return X coordinate of the destination
     */
    public double getDestX() {
        return destX;
    }

    /**
     * Returns the y coordinate of the destination
     * @return Y coordinate of the destination
     */
    public double getDestY() {
        return destY;
    }

    /**
     * Returns the route ID
     * @return The route ID
     */
    public int getBotRoute() {
        return this.routeID;
    }

    /**
     * Returns the bot ID
     * @return The bot ID
     */
    public int getBotNum() {
        return botID;
    }

    /**
     * Returns the current x coordinate
     * @return The current x coordinate
     */
    public double getX() {
        return this.xCoord;
    }

    /**
     * Returns the current y coordinate
     * @return The current y coordinate
     */
    public double getY() {
        return this.yCoord;
    }

    /**
     * Returns how long the bot has been exiting
     * @return How long the bot has been exiting
     */
    public int getHowLongSameRoom() {
        return howLongSameRoom;
    }

    /**
     * Returns the room ID
     * @return The room ID
     */
    public int getRoomID() {
        return this.roomID;
    }

    /**
     * Sets the room ID
     * @param roomID The new room ID
     */
    public void setRoomID(int roomID) {
        this.roomID = roomID;
    }

    /**
     * Sets the direction
     * @param direction The new direction
     */
    public void setDirection(double direction) {
        this.direction = direction;
    }

    /**
     * Sets the velocity
     * @param velocity The new velocity
     */
    public void setVelocity(double velocity) {
        this.velocity = velocity;
    }

    /**
     * Sets the coordinates
     * @param xCoord The new x coordinate
     * @param yCoord The new y coordinate
     */
    public void setCoords(double xCoord, double yCoord) {
        this.xCoord = xCoord;
        this.yCoord = yCoord;
    }

    /**
     * Returns the direction
     * @return The direction
     */
    public double getDirection() {
        return this.direction;
    }

    /**
     * Returns the velocity
     * @return The velocity
     */
    public double getVelocity() {
        return this.velocity;
    }

    /**
     * Returns the current exit in the route
     * @return The current exit
     */
    public Exit getCurrentWaypoint() {
        if (onStairs()) {
            Logger.getLogger("FireEscapeLog").log(Level.FINE, "Should use getStaircase instead");
            //System.out.println("Bot getCurrentWaypoint");
        }
        return GeneticAI.getRoute(this.getBotRoute()).getNavPoint(this.currentRouteWaypoint);
    }

    /**
     * Adds a neighbor
     * @param immed The new neighbor
     */
    public void addImmediateNeighbor(int immed) {
        immediateNeighbors.add(immed);
    }

    public void setImmediateNeighbors(ArrayList<Integer> immediateNeighbors) {
        this.immediateNeighbors = immediateNeighbors;
    }

    /**
     * Returns a neighbor
     * @param num The neighbor ID
     * @return The neighbor
     */
    public int getImmediateNeighbor(int num) {
        return immediateNeighbors.get(num);
    }

    /**
     * Returns the number of neighbors
     * @return The number of neighbors
     */
    public int getNumImmediateNeighbors() {
        return immediateNeighbors.size();
    }

    /*public void setNearestNeighbor(Bot bot) {
    nearestNeighbor = bot;
    }*/
    /**
     * Returns the nearest neighbor to this bot
     * @return The nearest neighbor to this bot
     */
    public Bot getNearestNeighbor() {
        //return nearestNeighbor;
        return BotAI.getNearestNeighbor(this);
    }

    /**
     * Resetes the neighbors
     */
    public void resetNeighbors() {
        immediateNeighbors = new ArrayList<Integer>();
        //nearestNeighbor = null;
    }

    /**
     * Paints this bot
     * @param g The Graphics2D object used to paint the bot
     */
    public void paint(Graphics2D g) {
        if (!this.escaped()) {
            //int t = (int) velocity * 30;
            //g.setColor(new Color(t, t, t));
            g.setColor(Color.blue);
            g.draw(new Ellipse2D.Double(FireEscapeGUI.scale * (this.getX() - BotAI.botRadius), FireEscapeGUI.scale * (this.getY() - BotAI.botRadius),
                    FireEscapeGUI.scale * BotAI.botRadius * 2, FireEscapeGUI.scale * BotAI.botRadius * 2));
        }
    }

    /*public boolean legitMove(double x, double y) {
    if (BotAI.distance(x, y, destX, destY) < 5) {
    return true;
    }
    Room room = Map.getRoom(roomID);
    double firstX = room.getX();
    double secondX = room.getX() + room.getWidth();
    double firstY = room.getY();
    double secondY = room.getY() + room.getHeight();
    return y - firstY > 3 && secondY - y > 3 && x - firstX > 3 && secondX - x > 3;
    }*/
    /**
     * Tests if a x move is within the bounds of the room
     * @param x The new x coordinate to be tested
     * @return True if it is within the bounds, false if it is not
     */
    /*public boolean legitXMove(double x) {
    if (BotAI.distance(x, yCoord, destX, destY) < 5) {
    return true;
    }
    Room room = Map.getRoom(roomID);
    double firstX = room.getX();
    double secondX = room.getX() + room.getWidth();
    return Math.abs(x - firstX) > BotAI.botRadius && Math.abs(x - secondX) > BotAI.botRadius;
    }*/
    /**
     * Tests if a y move is within the bounds of the room
     * @param y The new y coordinate to be tested
     * @return True if it is within the bounds, false if it is not
     */
    /*public boolean legitYMove(double y) {
    if (BotAI.distance(xCoord, y, destX, destY) < 5) {
    return true;
    }
    Room room = Map.getRoom(roomID);
    double firstY = room.getY();
    double secondY = room.getY() + room.getHeight();
    return Math.abs(y - firstY) > BotAI.botRadius && Math.abs(y - secondY) > BotAI.botRadius;
    }*/
    /**
     * Returns if the bot has escaped
     * @return True if the bot has escaped, false if not
     */
    public boolean escaped() {
        return escaped;
    }

    /**
     * Returns if the bot is moving through an exit
     * @return True if the bot is moving through an exit, false if not
     */
    public boolean exiting() {
        return exiting;
    }

    /**
     * Returns if the bot is on stairs
     * @return True if the bot is on stairs, false if not
     */
    public boolean onStairs() {
        return onStairs;
    }

    /**
     * Tests to see if the next exit is a Staircase
     * @return True if the next exit is a Staircase, false if not
     */
    public boolean exitIsStairs() {
        return this.getCurrentWaypoint() instanceof Staircase || transferringToStaircase || onStairs;
    }

    public boolean inRightPlace() {
        return roomID == 0 || (onStairs && getStaircase().onStaircaseLegit(this)) || Map.getRoom(roomID).botInRoom(this);
    }

    /*public void isNowApproaching() {
    //approachingExit = true;
    GeneticAI.getRoute(routeID).getNavPoint(currentRouteWaypoint).setApproaching(this);
    }
    
    public void nowNotApproaching() {
    //approachingExit = false;
    GeneticAI.getRoute(routeID).getNavPoint(currentRouteWaypoint).removeApproaching(this);
    }*/
    /**
     * Moves the bot to new coordinates
     */
    public void move() {
        //direction = BotAI.getMaxAdvantageousAngle(this);
        //if (BotAI.iterations % 30 == 0) {
        //}
        //if (!onStairs) {
        double x = calculateNewXCoord();
        double y = calculateNewYCoord();
        //if (legitMove(x, y)) {
        //if (legitXMove(x)) {
        xCoord = x;
        //}
        //if (legitYMove(y)) {
        yCoord = y;
        //}
        //}
        checkOnExit();
        howLongSameRoom++;
        /*} else {
        stairs();
        }*/
    }

    public boolean transferringToStaircase() {
        return transferringToStaircase;
    }

    public void moveOntoStairs() {
        Staircase stairs = (Staircase) getCurrentWaypoint();
        staircaseID = stairs.getStaircaseID();
        stairs.addBot(this);
        Room room = Map.getRoom(roomID);
        room.removeBot(this);
        transferringToStaircase = false;
        onStairs = true;
        howLongSameRoom = 0;
    }

    public boolean canMoveOntoStairs() {
        Exit waypoint = getCurrentWaypoint();
        if (!(waypoint instanceof Staircase)) {
            Logger.getLogger("FireEscapeLog").log(Level.FINE, "Exit should be staircase");
            //System.out.println("Bot canMoveOntoStairs");
        }
        Staircase stairs = (Staircase) waypoint;
        return stairs.canMoveOntoStairs(this);
    }

    /**
     * Checks to see if the bot is close enough to the next exit
     */
    public void checkOnExit() {
        if (!onStairs()) {
            Exit waypoint = this.getCurrentWaypoint();
            /*if (!(waypoint instanceof Staircase) && waypoint.distanceFromRectangle(getX(), getY()) < BotAI.botRadius + BotAI.defaultDistance) {
            isNowApproaching();
            }*/
            if (waypoint.getRect().containsBot(xCoord, yCoord)) {
                if (!exiting && !(waypoint instanceof Staircase)) {
                    shiftingAdding();
                }
                exiting = true;
                /*if (!(waypoint instanceof Staircase)) {
                setDirection(perpendicularToExit());
                }*/
            } else if (exiting) {// && (roomID == 0 || Map.getRoom(roomID).botInRoom(xCoord, yCoord, null))) {
                if (!(waypoint instanceof Staircase)) {
                    shiftRooms();
                } else {
                    transferringToStaircase = true;
                }
            }
            if (!inRightPlace()) {
                Logger.getLogger("FireEscapeLog").log(Level.FINE, "Bot is not in the correct room: " + this + "\n" + Map.getRoom(roomID));
                //System.out.println("Bot checkOnExit");
                //sigh Map.getRoom(roomID).botInRoom(this);
            }
        } else {
            if (getX() < 2 * BotAI.botRadius) {
                leaveStairs();
            }
        }
    }

    /**
     * Shifts rooms
     */
    public void shiftRooms() {
        /*if (!(waypoint instanceof Staircase)) {
        nowNotApproaching();
        }*/

        if (exitIsStairs()) {
            shiftingAdding();
        } else {
            Room lastRoom = Map.getRoom(lastRoomID);
            lastRoom.removeBot(this);
        }
        currentRouteWaypoint++;
        //System.out.println(room.getRoomID());

        if (roomID == 0) {
            escape();
        } else {
            setDestinations();
            setIdealDirection();
        }

        exiting = false;

        howLongSameRoom = 0;
    }

    /**
     * Adds the bot to the next room but does not delete the bot from the last room.
     */
    public void shiftingAdding() {
        Exit waypoint = this.getCurrentWaypoint();
        lastRoomID = roomID;
        roomID = waypoint.connectedToID(roomID);
        //int nextRoomID = waypoint.connectedToID(roomID);
        Room nextRoom = Map.getRoom(roomID);
        nextRoom.addBot(this);
    }

    /**
     * Method to be called if the bot is on stairs
     */
    /*public void stairs() {
    if (stairCount >= 10 && leavingStairsCheck()) {
    shiftRooms();
    onStairs = false;
    stairCount = -1;
    }
    stairCount++;
    }*/
    public void leaveStairs() {
        Staircase stairs = getStaircase();
        double distanceAlong = (getY() - BotAI.botRadius) / Staircase.staircaseHeight * (stairs.getLength() - 2 * BotAI.botRadius);
        double newX = stairs.getX1() + (distanceAlong + BotAI.botRadius) * Math.cos(stairs.getAngle());
        double newY = stairs.getY1() + (distanceAlong + BotAI.botRadius) * Math.sin(stairs.getAngle());
        if (leavingStairsCheck(newX, newY)) {
            stairs.removeBot(this);
            setCoords(newX, newY);
            onStairs = false;
            shiftRooms();
            staircaseID = -1;
            howLongSameRoom = 0;
        }
    }

    public boolean leavingStairsCheck(double x, double y) {
        Exit waypoint = getStaircase();
        Room nextRoom = Map.getRoom(waypoint.connectedToID(roomID));
        if (!nextRoom.botInRoom(x, y, null)) {
            Logger.getLogger("FireEscapeLog").log(Level.FINE, "Bot is not leaving into the correct room");
            //System.out.println("Bot leavingStairsCheck");
        }
        return nextRoom.totalNoOverlapCheck(x, y);
        /*Room room = Map.getRoom(roomID);
        return room.totalNoOverlapCheck(xCoord, yCoord);*/
    }

    public Staircase getStaircase() {
        return Map.getStaircase(staircaseID);
    }

    /*public boolean onTopOfLiveBot() {
    for (int botNum : immediateNeighbors) {
    Bot bot = BotAI.getBot(botNum);
    if (this.equals(bot)) {
    System.out.println("EPIC UBER FAIL");
    }
    if (!bot.onStairs() && BotAI.distance(bot, this) < BotAI.botRadius * 2) {
    return true;
    }
    }
    return false;
    }*/
    /**
     * Calculates the new x coordinate
     * @return The new x coordinate
     */
    public double calculateNewXCoord() {
        return this.getX() + (this.getVelocity() * Math.cos(this.getDirection()) * BotAI.timePerIteration);
    }

    /**
     * Calculates the new y coordinate
     * @return The new y coordinate
     */
    public double calculateNewYCoord() {
        return this.getY() + (this.getVelocity() * Math.sin(this.getDirection()) * BotAI.timePerIteration);
    }

    public double getIdealDirection() {
        return idealDirection;
    }

    public void setIdealDirection() {
        if (!exitIsStairs()) {
            Room room = Map.getRoom(roomID);
            idealDirection = BotAI.calculateAngle(room.getCenterX(), room.getCenterY(), destX, destY);
        } else {
            Exit exit = getCurrentWaypoint();
            idealDirection = BotAI.calculateAngle(getX(), getY(), exit.getCenterX(), exit.getCenterY());
        }
    }

    /**
     * Returns if other is equal to this
     * @param other The other object
     * @return True if the other has a bot ID equal to this bot ID
     */
    @Override
    public boolean equals(Object other) {
        if (!(other instanceof Bot)) {
            return false;
        }
        Bot bot = (Bot) other;
        return bot.botID == this.botID;
    }

    /**
     * Makes the bot escape
     */
    public void escape() {
        escaped = true;
        GeneticAI.getRoute(routeID).addTime(BotAI.iterations);
        FireEscape.escapeCount++;
        BotAI.botsEscaped++;
    }

    /**
     * Makes the bot die
     */
    public void die() {
        if (onStairs()) {
            getStaircase().removeBot(this);
            onStairs = false;
        } else if (exiting) {
            Map.getRoom(roomID).removeBot(this);
            if (lastRoomID != -1) {
                Map.getRoom(lastRoomID).removeBot(this);
            }
        } else {
            Map.getRoom(roomID).removeBot(this);
        }
        roomID = 0;
        escaped = true;
        exiting = false;
        howLongSameRoom = 0;
        //GeneticAI.getRoute(routeID).botDeath();
        FireEscape.deathCount++;
        BotAI.botsDied++;
    }

    /**
     * Prints out the ID, the x and y coordinates, and the room ID
     * @return Prints out the ID, the x and y coordinates, and the room ID
     */
    @Override
    public String toString() {
        return "Bot; ID: " + botID + " X: " + xCoord + " Y: " + yCoord + " Room: " + roomID + " OnStairs: " + onStairs;
    }
}
